/*
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
#pragma once

#include <unifex/receiver_concepts.hpp>
#include <unifex/sender_concepts.hpp>
#include <unifex/type_traits.hpp>

#include <unifex/detail/prologue.hpp>

namespace unifex {
namespace _create {

template <
    typename Receiver,
    typename Fn,
    typename Context,
    typename... ValueTypes>
struct _op {
  struct type {
    explicit type(Receiver rec, Fn fn, Context ctx) noexcept(
        std::is_nothrow_move_constructible_v<Receiver>&&
            std::is_nothrow_move_constructible_v<Fn>&&
                std::is_nothrow_move_constructible_v<Context>)
      : rec_((Receiver &&) rec)
      , fn_((Fn &&) fn)
      , ctx_((Context &&) ctx) {}

    template(typename... Ts)  //
        (requires(convertible_to<
                  Ts,
                  ValueTypes>&&...))  //
        void set_value(Ts&&... ts) noexcept {
      UNIFEX_TRY {
        // Satisfy the value completion contract by converting to the
        // Sender's value_types. For example, if set_value is called with
        // an lvalue reference but the create Sender sends non-reference
        // values.
        unifex::set_value(
            std::move(rec_), static_cast<ValueTypes>(static_cast<Ts&&>(ts))...);
      }
      UNIFEX_CATCH(...) {
        unifex::set_error(std::move(rec_), std::current_exception());
      }
    }

    template(typename Error)                  //
        (requires receiver<Receiver, Error>)  //
        void set_error(Error&& error) noexcept {
      unifex::set_error((Receiver &&) rec_, (Error &&) error);
    }

    void set_done() noexcept { unifex::set_done((Receiver &&) rec_); }

    template(class Ctx = Context)                    //
        (requires(!same_as<Ctx, detail::_empty<>>))  //
        Context const& context() const& noexcept {
      return ctx_;
    }

    template(class Ctx = Context)                    //
        (requires(!same_as<Ctx, detail::_empty<>>))  //
        Context&& context() && noexcept {
      return (Context &&) ctx_;
    }

  private:
    friend void tag_invoke(tag_t<start>, type& self) noexcept try {
      self.fn_(self);
    } catch (...) {
      unifex::set_error((Receiver &&) self.rec_, std::current_exception());
    }

    // Forward other receiver queries
    template(typename CPO)  //
        (requires is_receiver_query_cpo_v<CPO> AND
             std::is_invocable_v<CPO, const Receiver&>)  //
        friend auto tag_invoke(CPO cpo, const type& self) noexcept(
            std::is_nothrow_invocable_v<CPO, const Receiver&>)
            -> std::invoke_result_t<CPO, const Receiver&> {
      return std::move(cpo)(self.rec_);
    }

    UNIFEX_NO_UNIQUE_ADDRESS Receiver rec_;
    UNIFEX_NO_UNIQUE_ADDRESS Fn fn_;
    UNIFEX_NO_UNIQUE_ADDRESS Context ctx_;
  };
};

template <
    typename Receiver,
    typename Fn,
    typename Context,
    typename... ValueTypes>
using _operation = typename _op<Receiver, Fn, Context, ValueTypes...>::type;

template <typename Fn, typename Context, typename... ValueTypes>
struct _snd_base {
  struct type {
    template <template <typename...> class Variant>
    using error_types = Variant<std::exception_ptr>;

    static constexpr bool sends_done = true;

    // there's no way to know; maybe create() should have a way for the user to
    // specify
    static constexpr blocking_kind blocking = blocking_kind::maybe;

    // no way to know, but maybe there should be
    static constexpr bool is_always_scheduler_affine = false;

    template(typename Self, typename Receiver)  //
        (requires derived_from<remove_cvref_t<Self>, type> AND
             constructible_from<Fn, member_t<Self, Fn>> AND
                 constructible_from<Context, member_t<Self, Context>> AND
                     receiver_of<Receiver, ValueTypes...>)  //
        friend _operation<remove_cvref_t<Receiver>, Fn, Context, ValueTypes...> tag_invoke(
            tag_t<connect>,
            Self&& self,
            Receiver&& rec) noexcept(std::
                                         is_nothrow_constructible_v<
                                             _operation<
                                                 Receiver,
                                                 Fn,
                                                 Context,
                                                 ValueTypes...>,
                                             Receiver,
                                             member_t<Self, Fn>,
                                             member_t<Self, Context>>) {
      return _operation<remove_cvref_t<Receiver>, Fn, Context, ValueTypes...>{
          (Receiver &&) rec, ((Self &&) self).fn_, ((Self &&) self).ctx_};
    }

    UNIFEX_NO_UNIQUE_ADDRESS Fn fn_;
    UNIFEX_NO_UNIQUE_ADDRESS Context ctx_{};
  };
};

template <typename Fn, typename Context, typename... ValueTypes>
struct _snd {
  struct type : _snd_base<Fn, Context, ValueTypes...>::type {
    template <
        template <typename...>
        class Variant,
        template <typename...>
        class Tuple>
    using value_types = Variant<Tuple<ValueTypes...>>;
  };
};

template <typename Fn, typename... ValueTypes>
using _sender = typename _snd<Fn, detail::_empty<>, ValueTypes...>::type;

template <typename Fn, typename Context, typename... ValueTypes>
using _sender_with_context = typename _snd<Fn, Context, ValueTypes...>::type;

template <typename T>
T void_cast(void* pv) noexcept {
  return static_cast<T&&>(*static_cast<std::add_pointer_t<T>>(pv));
}

namespace _cpo {
template <typename... ValueTypes>
struct _fn {
  template(typename Fn)                  //
      (requires move_constructible<Fn>)  //
      _sender<Fn, ValueTypes...>
      operator()(Fn fn) const noexcept(
          std::is_nothrow_constructible_v<_sender<Fn, ValueTypes...>, Fn>) {
    return _sender<Fn, ValueTypes...>{{(Fn &&) fn}};
  }
  template(typename Fn, typename Context)                                //
      (requires move_constructible<Fn> AND move_constructible<Context>)  //
      _sender_with_context<Fn, Context, ValueTypes...>
      operator()(Fn fn, Context ctx) const
      noexcept(std::is_nothrow_constructible_v<
               _sender_with_context<Fn, Context, ValueTypes...>,
               Fn,
               Context>) {
    return _sender_with_context<Fn, Context, ValueTypes...>{
        {(Fn &&) fn, (Context &&) ctx}};
  }
};
}  // namespace _cpo
}  // namespace _create

/**
 * \fn template <class... ValueTypes> auto create(auto fn [, auto ctx])
 * \brief A utility for building a sender-based API out of a C-style API that
 *        accepts a void* context and a function pointer continuation.
 *
 * \em Example:
 * \code
 *  // A void-returning C-style async API that accepts a context and a
 * continuation: using callback_t = void(void* context, int result); void
 * old_c_style_api(int a, int b, void* context, callback_t* callback_fn);
 *
 *  // A sender-based async API implemented in terms of the C-style API (using
 * C++20): unifex::sender auto new_sender_api(int a, int b) { return
 * unifex::create<int>([=](auto& rec) { old_c_style_api(a, b, &rec, [](void*
 * context, int result) {
 *        unifex::void_cast<decltype(rec)>(context).set_value(result);
 *      });
 *    });
 *  }
 * \endcode
 * \tparam ValueTypes The value types of the resulting sender. Should be the
 * list of value type(s) accepted by the callback (with the exception of the
 * void* context). \param[in] fn A void-returning invocable that accepts an
 * lvalue reference to an object whose type satisfies the \c
 * unifex::receiver_of<ValueTypes...> concept. This function should dispatch to
 * the C-style callback (see example). \param[in] ctx An optional extra bit of
 * data to be bundled with the receiver passed to \c fn. E.g., if \c fn is a
 * lambda that accepts <tt>(auto& rec)</tt> and \c ctx is 42, then from within
 * the body of \c fn , the value of the expression \c rec.context() is 42.
 * \return A sender that, when connected and started, dispatches to the wrapped
 * C-style API with the callback of your choosing. The receiver passed to \c fn
 * wraps the receiver passed to \c connect . Your callback should "complete" the
 * receiver passed to \c fn , which will complete the receiver passed to \c
 * connect in turn.
 */
template <typename... ValueTypes>
inline constexpr _create::_cpo::_fn<ValueTypes...> create{};

using _create::void_cast;
}  // namespace unifex

#include <unifex/detail/epilogue.hpp>
